iT邦幫忙

2026 iThome 鐵人賽

DAY 30
0

29 天下來,SRS Review Agent 有了四層檢測、審查報告、命令列工具、Web 介面、Docker 部署,以及 Day 28-29 用測試守住的使用文件與 API 文件。最後一天要做兩件事:

  1. 讓別人能拿去用:README、授權、貢獻指南、版本紀錄、pip install 的套件設定,並把版本號定為 1.0.0
  2. 回顧這 30 天:哪些決定是對的、哪些數字比預期差,以及這個系統還不能做什麼

這一天在系列中的位置

第 4 週:發佈與開源

  • Day 22-24 → 審查報告、PDF、命令列工具
  • Day 25-26 → Docker、Docker Compose
  • Day 27 → 大規模性能基準
  • Day 28-29 → 使用文檔、API 文檔
  • Day 30 → 開源發佈與回顧 ← 今天

今日目標

讀完這篇後,你會完成:

  1. 開源專案的基本檔案:README.md、LICENSE、CONTRIBUTING.md、CHANGELOG.md、.gitignore
  2. 套件設定 pyproject.toml:pip install . 之後就有 srs-review 指令
  3. 版本 1.0.0:改一行,API、CLI、套件、Docker 映像檔一起變
  4. 發佈前檢查 tests/test_day30_release.py:6 個測試
  5. 發佈到 GitHub 的步驟
  6. 30 天回顧

問題背景:開源不只是把程式碼放上網

別人第一次看到這個專案時,會依序問:

問題 回答的檔案
這是什麼?做得好嗎? README.md
我可以用在公司的專案嗎? LICENSE
怎麼安裝? README.md → docs/USAGE.md、pyproject.toml
這一版改了什麼?有什麼已知問題? CHANGELOG.md
我想改進它,要注意什麼? CONTRIBUTING.md

還有一個看不見、但最重要的問題:不該公開的東西有沒有被一起放上去。這個專案的資料夾裡有 .env、開發用的資料庫 test.db(裡面有測試帳號)、Day 12 的 LLM 緩存。.gitignore 必須在第一次提交之前就寫好,一旦推上 GitHub,就算之後刪除,歷史紀錄裡也還找得到。


實現方法

src/__init__.py:__version__ = "1.0.0"   ← Day 29 建立的唯一版本來源
  ├─ API(/、/api/v1/health、/api/v1/info、OpenAPI)
  ├─ CLI(srs-review --version)
  ├─ pyproject.toml(dynamic version)
  └─ CHANGELOG.md 最新的版本(由測試檢查)

README.md  LICENSE  CONTRIBUTING.md  CHANGELOG.md  .gitignore  pyproject.toml
  ↓
tests/test_day30_release.py:版本一致、套件設定、README 連結、必要檔案、.gitignore
  • 輸入:Day 1-29 的程式與文件
  • 輸出:可以發佈的專案
  • 檔案:README.md、LICENSE、CONTRIBUTING.md、CHANGELOG.md、.gitignore、pyproject.toml、tests/test_day30_release.py(新增);src/__init__.py、docs/api/openapi.json(修改)

專案結構變化:

srs-review-agent/
├── README.md              ← 【新增】
├── LICENSE                ← 【新增】MIT
├── CONTRIBUTING.md        ← 【新增】
├── CHANGELOG.md           ← 【新增】
├── .gitignore             ← 【新增】
├── pyproject.toml         ← 【新增】
├── src/__init__.py        ← 修改:1.0.0
└── tests/
    └── test_day30_release.py  ← 【新增】6 個測試

代碼示例

1. README:先說限制

README.md 的開頭用一個例子說明系統在做什麼,接著是快速開始。比較特別的是「準確度與限制」這一段:

## 準確度與限制

在小型、刻意放入衝突的資料集上(3 份 SRS、15 個標準答案;Mistral 7B):

| 設定 | Precision | Recall | F1 |
| v1(規則 + 驗證 + 補充) | 0.905 | 0.722 | 0.767 |
| v1 驗證 + v2 補充 + guardrail(預設) | 0.905 | 0.917 | **0.897** |

**這個數字不代表在任意規格書上的表現。** 在較大、衝突較少的規格書上,品質會明顯下降:
- 58 條需求的真實規格書:LLM 補充層回報 17 個衝突,全部是誤報
- 合成規格書(Day 27):100 條需求時 F1 約 0.24;…

把 F1 0.897 放在 README 最顯眼的地方很容易,但 Day 24 與 Day 27 已經證明,這個數字只在小型、刻意放入衝突的資料集上成立。使用者拿真實的規格書來試,第一次就會遇到大量誤報。與其讓他們自己發現,不如在 README 就寫清楚,並告訴他們該怎麼用(--no-supplement、把結果當成線索)。

2. .gitignore

# 秘密與本機設定(.env.example 要提交)
.env

# 資料庫與緩存(含帳號與檢測結果)
*.db
.cache/

# 產生的報告(reports/ 中的實驗報告要提交)
*.report.pdf
*.report.md

# 前端
frontend/node_modules/
frontend/dist/

三個刻意的區分:

  • .env 不提交,.env.example 要提交:範本告訴別人需要哪些設定,真正的值留在本機
  • reports/ 中 Day 18-27 的實驗報告要提交:那是文章中所有數字的出處;CLI 產生的 *.report.pdf 則不提交
  • *.db 一律不提交:test.db 裡有開發時註冊的測試帳號與密碼雜湊

3. 套件設定

建立 pyproject.toml:

[project]
name = "srs-review-agent"
dynamic = ["version"]
requires-python = ">=3.12"
license = { text = "MIT" }
dependencies = [
    "fastapi>=0.104.0",
    "uvicorn>=0.24.0",
    ...
    "reportlab>=4.0",
]

[project.optional-dependencies]
dev = ["pytest>=7.0"]

[project.scripts]
srs-review = "src.cli:main"

[tool.setuptools]
packages = ["src"]

[tool.setuptools.dynamic]
version = { attr = "src.__version__" }
  • dynamic = ["version"]:版本號從 src.__version__ 讀取,不在這裡再寫一次。Day 29 統一了 API 與 CLI 的版本來源,現在套件也加入
  • [project.scripts]:安裝後產生 srs-review 指令,指向 Day 24 的 main()。python3 -m src.cli 依然可以用
  • dev 額外依賴:pytest 只有開發時需要,pip install -e ".[dev]" 才會安裝

requirements.txt 繼續保留:Day 25 的 Dockerfile 與既有的教學都用它。

4. 版本 1.0.0

只改一行:

# 單一版本來源:API(/、/api/v1/health、/api/v1/info、OpenAPI)、CLI(--version)
# 與 pyproject.toml(dynamic version)共用(Day 29-30)
__version__ = "1.0.0"   # Day 30:第一個公開版本

改完之後,Day 29 的 test_openapi_file_up_to_date 會立刻失敗:OpenAPI 規格裡的版本號變了,docs/api/openapi.json 卻還是 0.29.0。執行一次 python3 -m src.export_openapi 就好。這正是 Day 29 寫那個測試的目的。

5. 發佈前檢查

建立 tests/test_day30_release.py:

def test_changelog_matches_version():
    """測試 1:CHANGELOG 最新的版本就是 __version__"""
    changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
    latest = re.search(r"^## \[(\d+\.\d+\.\d+)\]", changelog, re.M).group(1)
    assert latest == __version__, f"CHANGELOG 最新版本 {latest},程式是 {__version__}"


def test_gitignore_protects_secrets():
    """測試 5:.env、資料庫、緩存、node_modules 不會被提交;.env.example 會"""
    ignored = (ROOT / ".gitignore").read_text(encoding="utf-8").split()
    for pattern in [".env", "*.db", ".cache/", "frontend/node_modules/", ".venv/"]:
        assert pattern in ignored, f".gitignore 缺少 {pattern}"
    assert ".env.example" not in ignored

另外 4 個測試檢查 pyproject.toml(動態版本、srs-review 指令、必要的相依套件)、README 的相對連結都存在、開源專案的基本檔案齊全、srs-review --version 的輸出。

6. 測試在 Docker 映像檔裡

重建 1.0.0 的映像檔,在容器裡跑測試時出現 2 個錯誤:Day 28、29 的文件測試找不到 docs/USAGE.md。原因是 Day 25 的 .dockerignore 刻意排除了 docs/,映像檔裡本來就不該有文件。

這三組測試(Day 28-30)檢查的是 repo 是否一致,不是執行環境能不能運作,所以在文件不存在時略過:

# 這是 repo 層級的檢查:Docker 映像檔依 .dockerignore 不含 docs/、examples/,在那裡略過(Day 30)
if not (ROOT / "docs" / "USAGE.md").exists():
    pytest.skip("文件不在此環境中(例如 Docker 映像檔)", allow_module_level=True)

驗證結果

測試

cd srs-review-agent
python3 -m pytest tests/ -q
135 passed in 4.68s

容器裡(Python 3.12):

docker build -t srs-review-agent:1.0.0 .
docker run --rm srs-review-agent:1.0.0 python -m pytest tests/ -q
# 117 passed, 3 skipped
docker run --rm srs-review-agent:1.0.0 python -m src.cli --version
# srs-review 1.0.0

略過的 3 個是 Day 28-30 的文件與發佈檢查(共 18 個測試),135 − 18 = 117。

從乾淨的環境安裝

python3 -m venv /tmp/fresh && /tmp/fresh/bin/pip install .
/tmp/fresh/bin/srs-review --version
# srs-review 1.0.0
/tmp/fresh/bin/srs-review analyze tests/fixtures/srs_small.md -o out.md
# 9 條需求,1 個衝突(高 1、中 0、低 0),待複核 1 個

發佈到 GitHub

這個資料夾目前還不是 git 倉庫。發佈的步驟如下(請在確認 .gitignore 後再執行):

cd srs-review-agent
git init
git add .
git status            # 確認沒有 .env、test.db、.cache/、node_modules/
git commit -m "SRS Review Agent 1.0.0"
git tag v1.0.0

# 在 GitHub 建立空的倉庫後
git remote add origin git@github.com:<帳號>/srs-review-agent.git
git push -u origin main --tags

git status 那一步不要省略:.gitignore 寫錯一個字,秘密就會被提交。推送之後,git tag v1.0.0 對應到 CHANGELOG.md 的 [1.0.0]。


30 天回顧

檢測品質的演進

階段 做法 Day 7-10 資料集 F1
Day 7 關鍵字規則(基線) 0.789
Day 8-10 擴展規則、語義相似度、混合 0.468 / 0.524 / 0.388
Day 11 Mistral 7B 驗證 + 補充 0.714
Day 12 批量驗證 + 緩存 0.767
Day 19 補充 prompt v2 0.659(被 JSON 錯誤抵銷)
Day 20 + 輸出 guardrail 0.897
Day 21 + 自我矛盾檢查 0.861(預設不開)

做對的決定

  • 一律用同一套資料、一律跑兩次:Day 11 發現原本的評測用的是另一套資料;Day 18 發現模型的說明文字在不同次執行間會變。之後所有比較都固定資料集、重複執行,數字才有意義
  • 先分析失敗,再改 prompt:Day 18 的分類指出誤報全部來自驗證層、漏報全部是兩層都沒找到。沒有這一步,Day 19 會在錯的地方用力
  • 分層對照:Day 19 的 v2 整體讓 F1 從 0.767 掉到 0.602;拆開兩層才發現補充層的改動其實有效,只是被一個 ], 吃掉了
  • 用呼叫次數衡量成本:同一個設定,耗時從 127 秒到 3091 秒都出現過,秒數幾乎沒有參考價值
  • 讓測試守住文件:Day 28-29 的同步檢查,在 Day 27 新增 OLLAMA_NUM_CTX 時立刻抓到文件漏寫

比預期差的地方

  • 小資料集的結論不能直接推廣:Day 7-10 的每份 SRS 只有 8-11 條需求、刻意放滿衝突。F1 0.897 是在這種條件下得到的。Day 24 的 58 條真實規格書上,補充層的 17 個結果全部錯誤;Day 27 的合成規格書在 100 條需求時 F1 只剩 0.24
  • 關鍵字規則不懂「對象」:A 模組的「多用戶」會和 B 模組的「單用戶」配對。Day 27 量到 1000 條需求時規則層產生 1156 個候選,而真正的規則型衝突只有 33 個
  • 小模型會附和、會照抄:Day 11 的驗證層附和 prompt 的暗示,Day 19 的補充層把 prompt 的定義句照抄進每一個理由
  • 上下文長度是硬限制:Day 27 發現超過 4096 token 的 prompt 會被默默截斷,模型只看到一半的需求,卻不會報錯

這個系統還不能做的事

  • 可靠地審查上百條需求的規格書(補充層誤報、prompt 截斷、規則候選爆炸)
  • 理解「性能取捨」「隱性安全風險」這類需要領域知識的衝突(Day 18 的 2 個爭議案例至今沒有解決)
  • 水平擴展:會話存在記憶體,只能有一個後端副本
  • 修復建議:只指出衝突,不建議怎麼改

如果繼續做下去

  1. 擴大評測資料:收集真實的規格書與人工標註,取代 15 個標準答案的小資料集。這是其他所有改進的前提
  2. 補充層分段處理:依章節分批送出,解決上下文長度與「一定要找到一些東西」的問題
  3. 規則加上對象比對:只配對描述同一個模組、同一類資料的需求
  4. 換更大的模型做比較:本系列只用了 Mistral 7B。驗證層的附和偏誤可能在更大的模型上消失,這需要 Day 21 的 A/B 框架來量測

提交變更

git add README.md LICENSE CONTRIBUTING.md CHANGELOG.md .gitignore pyproject.toml \
        src/__init__.py docs/api/openapi.json tests/
git commit -m "Day 30: 1.0.0 發佈(README、授權、貢獻指南、版本紀錄、套件設定)"

結語

30 天前,Day 1 的問題是:「能不能讓 Agent 幫忙找出規格書裡的矛盾?」現在的答案是:在小型規格書上可以,而且能找到規則找不到的衝突;在大型規格書上還不行,但我們知道確切的原因。

這個系列留下的不只是一個工具,還有一套做法:先建立評測、固定資料、逐筆分析失敗、一次只改一件事、用 A/B 決定要不要採用、讓測試守住文件。這套做法不依賴 Mistral,也不依賴這個題目,換一個模型、換一個領域都能沿用。

謝謝你讀到最後。


上一篇
Day 29:API 文檔與範例
系列文
解決需求規格書矛盾:用 Claude Code × MCP 實作自律型文檔審查 Agent 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言